<?php
/**
 * Oolala Framework
 * 
 * @category    Oolala
 * @package     Oolala_Acl
 * @subpackage  Oolala_Acl_Controller_Action_Helper
 * @copyright   Copyright (c) 2009 Thierry Jossermoz
 * @license     New BSD License
 */
class Oolala_Acl_Controller_Action_Helper_Acl extends Zend_Controller_Action_Helper_Abstract
{
    /**
     * Normalizer
     * 
     * @var Zend_Filter_Interface
     */
    protected static $_resourceIdNormalizer;
    
    /**
     * Acl
     * 
     * @var Zend_Acl
     */
    protected $_acl;

    /**
     * Role
     * 
     * @var Zend_Acl_Role_Interface
     */
    protected $_role;
    
    /**
     * Current resource
     * 
     * @var Zend_Acl_Resource_Interface
     */
    protected $_resource;
    
    /**
     * Current privilege
     * 
     * @var string
     */
    protected $_privilege;

    /**
     * Logger
     * 
     * @var Zend_Log
     */
    protected $_logger;
    
    /**
     * Resource Acl Context
     * 
     * @var Oolala_Acl_ResourceAcl
     */
    protected $_resourceAcl;
    
    /**
     * Constructor
     * 
     * @param array $options
     * @return void
     */
    public function __construct(array $options = array())
    {
        foreach($options as $key => $value) {
            $method = 'set' . ucfirst($key);
            if (method_exists($this, $method)) {
                $this->$method($value);
            }
        }
    }
    
    /**
     * Set the resourceId normalizer to filter the controller's class name
     * 
     * @param Zend_Filter_Interface $normalizer
     * @return void
     */
    public static function setResourceIdNormalizer(Zend_Filter_Interface $normalizer)
    {
        self::$_resourceIdNormalizer = $normalizer;
    }
    
    /**
     * Returns a normalized resourceId
     * A default normalizer is returned if none set
     * 
     * @return Zend_Filter_Interface
     */
    public static function getResourceIdNormalizer()
    {
        if (null === self::$_resourceIdNormalizer) {
            $normalizer = new Zend_Filter();
            $normalizer->addFilter(new Zend_Filter_PregReplace('/Controller$/'))
                       ->addFilter(new Zend_Filter_Word_CamelCaseToDash())
                       ->addFilter(new Zend_Filter_StringToLower())
                       ->addFilter(new Zend_Filter_Word_UnderscoreToSeparator(':'));

            self::$_resourceIdNormalizer = $normalizer;
        }
        
        return self::$_resourceIdNormalizer;
    }
    
    /**
     * Retrieve a normalized resource identifier
     * 
     * @param Zend_Controller_Action $controller
     * @return string
     */
    public static function getNormalizedResourceId(Zend_Controller_Action $controller)
    {
        return self::getResourceIdNormalizer()->filter(get_class($controller));
    }
    
    /**
     * (Re)Initialize the helper
     * 
     * @see Zend/Controller/Action/Helper/Zend_Controller_Action_Helper_Abstract#init()
     */
    public function init()
    {
        $this->_resourceAcl = null;
        $this->_resource = null;
        $this->_privilege = null;

        if ($this->getActionController() instanceof Zend_Acl_Resource_Interface) {
            $this->_resource = $this->getActionController();
            $this->_privilege = $this->getRequest()->getActionName();
        }
    }

    /**
     * Set the ACL to use
     * 
     * @param Zend_Acl $acl
     * @return Oolala_Acl_Controller_Action_Helper_Acl
     */
    public function setAcl(Zend_Acl $acl)
    {
        $this->_acl = $acl;

        return $this;
    }
    
    /**
     * Retrieve Zend_Acl instance
     * 
     * @return Zend_Acl
     */
    public function getAcl()
    {
        return $this->_acl;
    }

    /**
     * Set the role to be checked against
     * 
     * @param Zend_Acl_Role_Interface $role
     * @return Oolala_Acl_Controller_Action_Helper_Acl
     */
    public function setRole(Zend_Acl_Role_Interface $role)
    {
        $this->_role = $role;

        return $this;
    }

    /**
     * Get role
     * 
     * @return Zend_Acl_Role_Interface
     */
    public function getRole()
    {
        return $this->_role;
    }

    /**
     * Retrieve the resource
     * 
     * @return null|Zend_Acl_Resource_Interface
     */
    public function getResource()
    {
        return $this->_resource;
    }

    /**
     * Retrieve the privilege
     * 
     *  @return null|string
     */
    public function getPrivilege()
    {
       return $this->_privilege;
    }

    /**
     * Get the Resource's Acl context object
     * 
     * @return Oolala_Acl_ResourceAcl
     * @throws Zend_Controller_Action_Exception when not in a controller context, when the controller does not implement Zend_Acl_Resource_Interface or when ACLs are not set
     */
    public function getResourceAcl()
    {
    	if (null === $this->_resourceAcl) {

    	    if (!$this->getResource() instanceof Zend_Acl_Resource_Interface) {
	    		throw new Zend_Controller_Action_Exception('getResourceAcl() must be called from a controller implementing Zend_Acl_Resource_Interface.');
	    	}

	    	if (null === $this->getAcl()) {
	    		throw new Zend_Controller_Action_Exception('setAcl() must be called prior to getResourceAcl().');
	    	}

    		$this->_resourceAcl = new Oolala_Acl_ResourceAcl($this->getActionController(), $this->getAcl());
    	}

    	return $this->_resourceAcl;
    }

    /**
     * Set logger
     * 
     * @param Zend_Log $logger
     * @return Oolala_Acl_Controller_Action_Helper_Acl
     */
    public function setLogger(Zend_Log $logger)
    {
        $this->_logger = $logger;
        
        return $this;
    }
    
    /**
     * Retrieve logger
     * 
     * @return Zend_Log
     */
    public function getLogger()
    {
        return $this->_logger;
    }

    /**
     * preDispatch performs the acl check 
     * 
     * @uses Zend_Acl::isAllowed()
     * @return void
     * @throws Zend_Acl_Exception when role or resource are not known
     * @throws Zend_Controller_Action_Exception when Acls are not set
     */
    public function preDispatch()
    {
        if (null === $this->getResource()) {
            // No resource to check, Allowed
            return;
        }

    	if (null === $this->getAcl()) {
    	    // Acls should be present
    		throw new Zend_Controller_Action_Exception('setAcl() must be called prior to preDispatch().');
    	}

    	if (!$this->_acl->has($this->getResource())) {
    	    // If resource doesn't exist, try to check whether rules are declared as docblocks
    	    $this->_parseDocblocks();
    	}
    	
        if (true === $this->getAcl()->isAllowed($this->getRole(), $this->getResource(), $this->getPrivilege())) {
            // Role is allowed on resource/privilege, Allow 
            return;            
        }

        // The role isn't allowed on specified resource and privilege, Deny
        $this->denyAccess("This role is not allowed to access this resource or privilege.");
    }

    /**
     * Deny access by throwing an Unauthorized Exception with code 401
     * 
     * @param string $message
     * @throws Oolala_Acl_Controller_Action_UnauthorizedException
     */
    public function denyAccess($message = null)
    {
        if ($this->getLogger()) {

            $role = null;
            if (null !== $this->getRole()) {
                $role = $this->getRole()->getRoleId();
            }
            
            if (null !== $this->getResource()) {

                $logMessage = sprintf('Access denied to role \'%1$s\' on resource \'%2$s\', privilege \'%3$s\' with message: \'%4$s\'',
                                      $role,
                                      $this->getResource()->getResourceId(),
                                      $this->getPrivilege(),
                                      $message);

            } else {

                $logMessage = sprintf('Access denied to role \'%1$s\' in module \'%2$s\', controller \'%3$s\', action \'%4$s\' with message: \'%5$s\'',
                                      $role,
                                      $this->getRequest()->getModuleName(),
                                      $this->getRequest()->getControllerName(),
                                      $this->getRequest()->getActionName(),
                                      $message);

            }

            // log alert
            $this->getLogger()->log($logMessage, Zend_Log::ALERT);
        }

        // throw exception and let the error handler plugin do its job
        throw new Oolala_Acl_Controller_Action_Helper_Acl_UnauthorizedException($message, 401);
    }

    /**
     * Strategy pattern
     * 
     * @return Oolala_Acl_ResourceAcl
     */
    public function direct()
    {
        return $this->getResourceAcl();
    }

    /**
     * Parse the docblocks of the controller and set its acl rules
     * 
     * @return void
     */
    protected function _parseDocblocks()
    {
        $docblocks = array();
        $parent = null;
        
        // Get Controller's rules
        $class = new Zend_Reflection_Class($this->getResource());
        try {
        	
        	$docblock = $class->getDocblock();                
            $docblocks[] = $docblock;
            
            if ($docblock->hasTag('inherits')) {
            	$parent = $this->_parseDescription($docblock, 'inherits');
            }
            
        } catch (Zend_Reflection_Exception $e) {}

        // Get Actions's rules
        $method = $class->getMethod(Zend_Filter::filterStatic($this->getPrivilege(), 'Word_DashToCamelCase') . 'Action');
        try {
        	
            $docblocks[$this->getPrivilege()] = $method->getDocBlock();
        
        } catch (Zend_Reflection_Exception $e) {}

        // if rules are found
        if (!empty($docblocks)) {
            // Add Controller to ACLs
            $this->getAcl()->addResource($this->getResource(), $parent);
            
            // Add Controller and Action rules
            foreach($docblocks as $privilege => $docblock) {
                $privilege = is_string($privilege) ? $privilege : null;
                if ($docblock->hasTag('allow')) {
                    $this->getAcl()->allow(
                        $this->_parseDescription($docblock, 'allow'),
                        $this->getResource(),
                        $privilege
                    );
                }
                if ($docblock->hasTag('deny')) {
                    $this->getAcl()->deny(
                        $this->_parseDescription($docblock, 'deny'),
                        $this->getResource(),
                        $privilege
                    );
                }
            }
        }
    }

    /**
     * Parse docblock description
     * 
     * @param Zend_Reflection_Docblock $docblock
     * @param string $rule
     * @return array|null
     */
    protected function _parseDescription($docblock, $rule)
    {
        $description = $docblock->getTag($rule)->getDescription();
        
        if (null !== $description) {
            
        	$description = explode(',', Zend_Filter::filterStatic($description, 'PregReplace', array('/\s/', '')));
            
            if (count($description) === 1) {
            	return $description[0];
            }
            
            return $description;

        }
        
        return null;
    }
}